#include <test/jtx.h>

#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>

namespace ripple {

namespace test {

class SetTrust_test : public beast::unit_test::suite
{
public:
    void
    testTrustLineDelete()
    {
        testcase(
            "Test deletion of trust lines: revert trust line limit to zero");

        using namespace jtx;
        Env env(*this);

        Account const alice = Account{"alice"};
        Account const becky = Account{"becky"};

        env.fund(XRP(10000), becky, alice);
        env.close();

        // becky wants to hold at most 50 tokens of alice["USD"]
        // becky is the customer, alice is the issuer
        // becky can be sent at most 50 tokens of alice's USD
        env(trust(becky, alice["USD"](50)));
        env.close();

        // Since the settings of the trust lines are non-default for both
        // alice and becky, both of them will be charged an owner reserve
        // Irrespective of whether the issuer or the customer initiated
        // the trust-line creation, both will be charged
        env.require(lines(alice, 1));
        env.require(lines(becky, 1));

        // Fetch the trust-lines via RPC for verification
        Json::Value jv;
        jv["account"] = becky.human();
        auto beckyLines = env.rpc("json", "account_lines", to_string(jv));

        jv["account"] = alice.human();
        auto aliceLines = env.rpc("json", "account_lines", to_string(jv));

        BEAST_EXPECT(aliceLines[jss::result][jss::lines].size() == 1);
        BEAST_EXPECT(beckyLines[jss::result][jss::lines].size() == 1);

        //         reset the trust line limits to zero
        env(trust(becky, alice["USD"](0)));
        env.close();

        // the reset of the trust line limits deletes the trust-line
        // this occurs despite the authorization of the trust-line by the
        // issuer(alice, in this unit test)
        env.require(lines(becky, 0));
        env.require(lines(alice, 0));

        // second verification check via RPC calls
        jv["account"] = becky.human();
        beckyLines = env.rpc("json", "account_lines", to_string(jv));

        jv["account"] = alice.human();
        aliceLines = env.rpc("json", "account_lines", to_string(jv));

        BEAST_EXPECT(aliceLines[jss::result][jss::lines].size() == 0);
        BEAST_EXPECT(beckyLines[jss::result][jss::lines].size() == 0);

        // additionally, verify that account_objects is an empty array
        jv["account"] = becky.human();
        auto const beckyObj = env.rpc("json", "account_objects", to_string(jv));
        BEAST_EXPECT(beckyObj[jss::result][jss::account_objects].size() == 0);

        jv["account"] = alice.human();
        auto const aliceObj = env.rpc("json", "account_objects", to_string(jv));
        BEAST_EXPECT(aliceObj[jss::result][jss::account_objects].size() == 0);
    }

    void
    testTrustLineResetWithAuthFlag()
    {
        testcase(
            "Reset trust line limit with Authorised Lines: Verify "
            "deletion of trust lines");

        using namespace jtx;
        Env env(*this);

        Account const alice = Account{"alice"};
        Account const becky = Account{"becky"};

        env.fund(XRP(10000), becky, alice);
        env.close();

        // alice wants to ensure that all holders of her tokens are authorised
        env(fset(alice, asfRequireAuth));
        env.close();

        // becky wants to hold at most 50 tokens of alice["USD"]
        // becky is the customer, alice is the issuer
        // becky can be sent at most 50 tokens of alice's USD
        env(trust(becky, alice["USD"](50)));
        env.close();

        // alice authorizes becky to hold alice["USD"] tokens
        env(trust(alice, alice["USD"](0), becky, tfSetfAuth));
        env.close();

        // Since the settings of the trust lines are non-default for both
        // alice and becky, both of them will be charged an owner reserve
        // Irrespective of whether the issuer or the customer initiated
        // the trust-line creation, both will be charged
        env.require(lines(alice, 1));
        env.require(lines(becky, 1));

        // Fetch the trust-lines via RPC for verification
        Json::Value jv;
        jv["account"] = becky.human();
        auto beckyLines = env.rpc("json", "account_lines", to_string(jv));

        jv["account"] = alice.human();
        auto aliceLines = env.rpc("json", "account_lines", to_string(jv));

        BEAST_EXPECT(aliceLines[jss::result][jss::lines].size() == 1);
        BEAST_EXPECT(beckyLines[jss::result][jss::lines].size() == 1);

        //         reset the trust line limits to zero
        env(trust(becky, alice["USD"](0)));
        env.close();

        // the reset of the trust line limits deletes the trust-line
        // this occurs despite the authorization of the trust-line by the
        // issuer(alice, in this unit test)
        env.require(lines(becky, 0));
        env.require(lines(alice, 0));

        // second verification check via RPC calls
        jv["account"] = becky.human();
        beckyLines = env.rpc("json", "account_lines", to_string(jv));

        jv["account"] = alice.human();
        aliceLines = env.rpc("json", "account_lines", to_string(jv));

        BEAST_EXPECT(aliceLines[jss::result][jss::lines].size() == 0);
        BEAST_EXPECT(beckyLines[jss::result][jss::lines].size() == 0);
    }

    void
    testFreeTrustlines(
        FeatureBitset features,
        bool thirdLineCreatesLE,
        bool createOnHighAcct)
    {
        if (thirdLineCreatesLE)
            testcase("Allow two free trustlines");
        else
            testcase("Dynamic reserve for trustline");

        using namespace jtx;
        Env env(*this, features);

        auto const gwA = Account{"gwA"};
        auto const gwB = Account{"gwB"};
        auto const acctC = Account{"acctC"};
        auto const acctD = Account{"acctD"};

        auto const& creator = createOnHighAcct ? acctD : acctC;
        auto const& assistor = createOnHighAcct ? acctC : acctD;

        auto const txFee = env.current()->fees().base;
        auto const baseReserve = env.current()->fees().reserve;
        auto const threelineReserve = env.current()->fees().accountReserve(3);

        env.fund(XRP(10000), gwA, gwB, assistor);

        // Fund creator with ...
        env.fund(
            baseReserve /* enough to hold an account */
                + drops(3 * txFee) /* and to pay for 3 transactions */,
            creator);

        env(trust(creator, gwA["USD"](100)), require(lines(creator, 1)));
        env(trust(creator, gwB["USD"](100)), require(lines(creator, 2)));

        if (thirdLineCreatesLE)
        {
            // creator does not have enough for the third trust line
            env(trust(creator, assistor["USD"](100)),
                ter(tecNO_LINE_INSUF_RESERVE),
                require(lines(creator, 2)));
        }
        else
        {
            // First establish opposite trust direction from assistor
            env(trust(assistor, creator["USD"](100)),
                require(lines(creator, 3)));

            // creator does not have enough to create the other direction on
            // the existing trust line ledger entry
            env(trust(creator, assistor["USD"](100)),
                ter(tecINSUF_RESERVE_LINE));
        }

        // Fund creator additional amount to cover
        env(pay(env.master, creator, STAmount{threelineReserve - baseReserve}));

        if (thirdLineCreatesLE)
        {
            env(trust(creator, assistor["USD"](100)),
                require(lines(creator, 3)));
        }
        else
        {
            env(trust(creator, assistor["USD"](100)),
                require(lines(creator, 3)));

            Json::Value jv;
            jv["account"] = creator.human();
            auto const lines = env.rpc("json", "account_lines", to_string(jv));
            // Verify that all lines have 100 limit from creator
            BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
            BEAST_EXPECT(lines[jss::result][jss::lines].size() == 3);
            for (auto const& line : lines[jss::result][jss::lines])
            {
                BEAST_EXPECT(line[jss::limit] == "100");
            }
        }
    }

    void
    testTicketSetTrust(FeatureBitset features)
    {
        testcase("SetTrust using a ticket");

        using namespace jtx;

        //  Verify that TrustSet transactions can use tickets.
        Env env{*this, features};
        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        auto const USD = gw["USD"];

        env.fund(XRP(10000), gw, alice);
        env.close();

        // Cannot pay alice without a trustline.
        env(pay(gw, alice, USD(200)), ter(tecPATH_DRY));
        env.close();

        // Create a ticket.
        std::uint32_t const ticketSeq{env.seq(alice) + 1};
        env(ticket::create(alice, 1));
        env.close();

        // Use that ticket to create a trust line.
        env(trust(alice, USD(1000)), ticket::use(ticketSeq));
        env.close();

        // Now the payment succeeds.
        env(pay(gw, alice, USD(200)));
        env.close();
    }

    Json::Value
    trust_explicit_amt(jtx::Account const& a, STAmount const& amt)
    {
        Json::Value jv;
        jv[jss::Account] = a.human();
        jv[jss::LimitAmount] = amt.getJson(JsonOptions::none);
        jv[jss::TransactionType] = jss::TrustSet;
        jv[jss::Flags] = 0;
        return jv;
    }

    void
    testMalformedTransaction(FeatureBitset features)
    {
        testcase("SetTrust checks for malformed transactions");

        using namespace jtx;
        Env env{*this, features};

        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        env.fund(XRP(10000), gw, alice);

        // Require valid tf flags
        for (std::uint64_t badFlag = 1u;
             badFlag <= std::numeric_limits<std::uint32_t>::max();
             badFlag *= 2)
        {
            if (badFlag & tfTrustSetMask)
                env(trust(
                        alice,
                        gw["USD"](100),
                        static_cast<std::uint32_t>(badFlag)),
                    ter(temINVALID_FLAG));
        }

        // trust amount can't be XRP
        env(trust_explicit_amt(alice, drops(10000)), ter(temBAD_LIMIT));

        // trust amount can't be badCurrency IOU
        env(trust_explicit_amt(alice, gw[to_string(badCurrency())](100)),
            ter(temBAD_CURRENCY));

        // trust amount can't be negative
        env(trust(alice, gw["USD"](-1000)), ter(temBAD_LIMIT));

        // trust amount can't be from invalid issuer
        env(trust_explicit_amt(
                alice, STAmount{Issue{to_currency("USD"), noAccount()}, 100}),
            ter(temDST_NEEDED));

        // trust cannot be to self
        env(trust(alice, alice["USD"](100)), ter(temDST_IS_SRC));

        // tfSetAuth flag should not be set if not required by lsfRequireAuth
        env(trust(alice, gw["USD"](100), tfSetfAuth), ter(tefNO_AUTH_REQUIRED));
    }

    void
    testExceedTrustLineLimit()
    {
        testcase(
            "Ensure that trust line limits are respected in payment "
            "transactions");

        using namespace jtx;
        Env env{*this};

        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        env.fund(XRP(10000), gw, alice);

        // alice wants to hold at most 100 of gw's USD tokens
        env(trust(alice, gw["USD"](100)));
        env.close();

        // send a payment for a large quantity through the trust line
        env(pay(gw, alice, gw["USD"](200)), ter(tecPATH_PARTIAL));
        env.close();

        // on the other hand, smaller payments should succeed
        env(pay(gw, alice, gw["USD"](20)));
        env.close();
    }

    void
    testAuthFlagTrustLines()
    {
        testcase(
            "Ensure that authorised trust lines do not allow payments "
            "from unauthorised counter-parties");

        using namespace jtx;
        Env env{*this};

        auto const bob = Account{"bob"};
        auto const alice = Account{"alice"};
        env.fund(XRP(10000), bob, alice);

        // alice wants to ensure that all holders of her tokens are authorised
        env(fset(alice, asfRequireAuth));
        env.close();

        // create a trust line from bob to alice. bob wants to hold at most
        // 100 of alice's USD tokens. Note: alice hasn't authorised this
        // trust line yet.
        env(trust(bob, alice["USD"](100)));
        env.close();

        // send a payment from alice to bob, validate that the payment fails
        env(pay(alice, bob, alice["USD"](10)), ter(tecPATH_DRY));
        env.close();
    }

    void
    testTrustLineLimitsWithRippling()
    {
        testcase(
            "Check that trust line limits are respected in conjunction "
            "with rippling feature");

        using namespace jtx;
        Env env{*this};

        auto const bob = Account{"bob"};
        auto const alice = Account{"alice"};
        env.fund(XRP(10000), bob, alice);

        // create a trust line from bob to alice. bob wants to hold at most
        // 100 of alice's USD tokens.
        env(trust(bob, alice["USD"](100)));
        env.close();

        // archetypical payment transaction from alice to bob must succeed
        env(pay(alice, bob, alice["USD"](20)), ter(tesSUCCESS));
        env.close();

        // Issued tokens are fungible. i.e. alice's USD is identical to bob's
        // USD
        env(pay(bob, alice, bob["USD"](10)), ter(tesSUCCESS));
        env.close();

        // bob cannot place alice in his debt i.e. alice's balance of the USD
        // tokens cannot go below zero.
        env(pay(bob, alice, bob["USD"](11)), ter(tecPATH_PARTIAL));
        env.close();

        // payments that respect the trust line limits of alice should succeed
        env(pay(bob, alice, bob["USD"](10)), ter(tesSUCCESS));
        env.close();
    }

    void
    testModifyQualityOfTrustline(
        FeatureBitset features,
        bool createQuality,
        bool createOnHighAcct)
    {
        testcase << "SetTrust " << (createQuality ? "creates" : "removes")
                 << " quality of trustline for "
                 << (createOnHighAcct ? "high" : "low") << " account";

        using namespace jtx;
        Env env{*this, features};

        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};

        auto const& fromAcct = createOnHighAcct ? alice : bob;
        auto const& toAcct = createOnHighAcct ? bob : alice;

        env.fund(XRP(10000), fromAcct, toAcct);

        auto txWithoutQuality = trust(toAcct, fromAcct["USD"](100));
        txWithoutQuality["QualityIn"] = "0";
        txWithoutQuality["QualityOut"] = "0";

        auto txWithQuality = txWithoutQuality;
        txWithQuality["QualityIn"] = "1000";
        txWithQuality["QualityOut"] = "1000";

        auto& tx1 = createQuality ? txWithQuality : txWithoutQuality;
        auto& tx2 = createQuality ? txWithoutQuality : txWithQuality;

        auto check_quality = [&](bool const exists) {
            Json::Value jv;
            jv["account"] = toAcct.human();
            auto const lines = env.rpc("json", "account_lines", to_string(jv));
            auto quality = exists ? 1000 : 0;
            BEAST_EXPECT(lines[jss::result][jss::lines].isArray());
            BEAST_EXPECT(lines[jss::result][jss::lines].size() == 1);
            BEAST_EXPECT(
                lines[jss::result][jss::lines][0u][jss::quality_in] == quality);
            BEAST_EXPECT(
                lines[jss::result][jss::lines][0u][jss::quality_out] ==
                quality);
        };

        env(tx1, require(lines(toAcct, 1)), require(lines(fromAcct, 1)));
        check_quality(createQuality);

        env(tx2, require(lines(toAcct, 1)), require(lines(fromAcct, 1)));
        check_quality(!createQuality);
    }

    void
    testDisallowIncoming(FeatureBitset features)
    {
        testcase("Create trustline with disallow incoming");

        using namespace test::jtx;

        // fixDisallowIncomingV1
        {
            for (bool const withFix : {true, false})
            {
                auto const amend =
                    withFix ? features : features - fixDisallowIncomingV1;

                Env env{*this, amend};
                auto const dist = Account("dist");
                auto const gw = Account("gw");
                auto const USD = gw["USD"];
                auto const distUSD = dist["USD"];

                env.fund(XRP(1000), gw, dist);
                env.close();

                env(fset(gw, asfRequireAuth));
                env.close();

                env(fset(dist, asfDisallowIncomingTrustline));
                env.close();

                env(trust(dist, USD(10000)));
                env.close();

                // withFix: can set trustline
                // withOutFix: cannot set trustline
                auto const trustResult =
                    withFix ? ter(tesSUCCESS) : ter(tecNO_PERMISSION);
                env(trust(gw, distUSD(10000)),
                    txflags(tfSetfAuth),
                    trustResult);
                env.close();

                auto const txResult =
                    withFix ? ter(tesSUCCESS) : ter(tecPATH_DRY);
                env(pay(gw, dist, USD(1000)), txResult);
                env.close();
            }
        }

        Env env{*this, features};

        auto const gw = Account{"gateway"};
        auto const alice = Account{"alice"};
        auto const bob = Account{"bob"};
        auto const USD = gw["USD"];

        env.fund(XRP(10000), gw, alice, bob);
        env.close();

        // Set flag on gateway
        env(fset(gw, asfDisallowIncomingTrustline));
        env.close();

        // Create a trustline which will fail
        env(trust(alice, USD(1000)), ter(tecNO_PERMISSION));
        env.close();

        // Unset the flag
        env(fclear(gw, asfDisallowIncomingTrustline));
        env.close();

        // Create a trustline which will now succeed
        env(trust(alice, USD(1000)));
        env.close();

        // Now the payment succeeds.
        env(pay(gw, alice, USD(200)));
        env.close();

        // Set flag on gateway again
        env(fset(gw, asfDisallowIncomingTrustline));
        env.close();

        // Destroy the balance by sending it back
        env(pay(gw, alice, USD(200)));
        env.close();

        // The trustline still exists in default state
        // So a further payment should work
        env(pay(gw, alice, USD(200)));
        env.close();

        // Also set the flag on bob
        env(fset(bob, asfDisallowIncomingTrustline));
        env.close();

        // But now bob can't open a trustline because he didn't already have one
        env(trust(bob, USD(1000)), ter(tecNO_PERMISSION));
        env.close();

        // The gateway also can't open this trustline because bob has the flag
        // set
        env(trust(gw, bob["USD"](1000)), ter(tecNO_PERMISSION));
        env.close();

        // Unset the flag only on the gateway
        env(fclear(gw, asfDisallowIncomingTrustline));
        env.close();

        // Now bob can open a trustline
        env(trust(bob, USD(1000)));
        env.close();

        // And the gateway can send bob a balance
        env(pay(gw, bob, USD(200)));
        env.close();
    }

    void
    testWithFeats(FeatureBitset features)
    {
        testFreeTrustlines(features, true, false);
        testFreeTrustlines(features, false, true);
        testFreeTrustlines(features, false, true);
        // true, true case doesn't matter since creating a trustline ledger
        // entry requires reserve from the creator
        // independent of hi/low account ids for endpoints
        testTicketSetTrust(features);
        testMalformedTransaction(features);
        testModifyQualityOfTrustline(features, false, false);
        testModifyQualityOfTrustline(features, false, true);
        testModifyQualityOfTrustline(features, true, false);
        testModifyQualityOfTrustline(features, true, true);
        testDisallowIncoming(features);
        testTrustLineResetWithAuthFlag();
        testTrustLineDelete();
        testExceedTrustLineLimit();
        testAuthFlagTrustLines();
        testTrustLineLimitsWithRippling();
    }

public:
    void
    run() override
    {
        using namespace test::jtx;
        auto const sa = testable_amendments();
        testWithFeats(sa);
    }
};
BEAST_DEFINE_TESTSUITE(SetTrust, app, ripple);
}  // namespace test
}  // namespace ripple
